﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Xml;
using Common.Utilities;
using Common.Utilities.Dispatcher;
using PdfLib.TextScraping;

namespace Workflows.Components.TextMining.EnumerationObjects
{
    /// <summary>
    /// 
    /// </summary>
    public class TableLayoutDetectorWorker:IWorker
    {
        #region fields
        private Thread _WorkerThread;
        private Guid _JobTicketID;
        private TableLayoutDetectorInstruction _Instruction;
        private TableLayoutDetectorOutput _Output;
        private RunContext _Context;
        private string _WorkflowName;
        private string _ComponentName;

        /// <summary>
        /// 
        /// </summary>
        public string ComponentName
        {
            get { return _ComponentName; }
        }

        /// <summary>
        /// 
        /// </summary>
        public string WorkflowName
        {
            get { return _WorkflowName; }
        }
        private object _SyncObj;
        private StreamWriter _LogWriter;
        private StreamWriter _ErrorWriter;
        private Dictionary<int, string> _DocIDFilePathMappings;
        private XmlTextWriter _OutputWriter;
        private ManualResetEvent[] _ManualResets;
        private int[] _CurrentWorkloads;
        private Dictionary<int, PdfScanOutput.PdfScanResult> _PagesContainingTable;
        private Dictionary<int, TableColumnDetectorInstruction> _ColumnDetectSettings;
        #endregion

        #region IWorker Members

        /// <summary>
        /// handler to worker thread
        /// </summary>
        public Thread WorkerThread
        {
            get { return _WorkerThread; }
        }

        /// <summary>
        /// allow instruction to be changed at runtime
        /// </summary>
        public InstructionBase Instruction
        {
            get { return _Instruction; }
        }

        /// <summary>
        /// output
        /// </summary>
        public ResultBase Output
        {
            get { return _Output; }
        }

        /// <summary>
        /// allow worker to be created based on component type
        /// </summary>
        public Type ComponentType
        {
            get { return typeof(TableLayoutDetectorComponent); }
        }

        /// <summary>
        /// runtime context retrieved from appl domain
        /// </summary>
        public RunContext Context
        {
            get { return this._Context; }
            set { this._Context = value; }
        }

        /// <summary>
        /// this must be called before actual work
        /// </summary>
        /// <param name="ticketGuid"></param>
        /// <param name="request"></param>
        public void SetInstruction(Guid ticketGuid, InstructionBase request, string workflowName, string componentName)
        {
            this._JobTicketID = ticketGuid;
            this._Instruction = (TableLayoutDetectorInstruction)request;
            this._WorkflowName = workflowName;
            this._ComponentName = componentName;
        }

        /// <summary>
        /// this is called within subflow, where runtime context is also initialized
        /// </summary>
        /// <param name="context"></param>
        /// <param name="ticketGuid"></param>
        /// <param name="request"></param>
        public void SetInstruction(RunContext context, Guid ticketGuid, InstructionBase request, string workflowName, string componentName)
        {
            this._JobTicketID = ticketGuid;
            this._Instruction = (TableLayoutDetectorInstruction)request;
            this._Context = context;
            this._WorkflowName = workflowName;
            this._ComponentName = componentName;
        }

        /// <summary>
        /// this is called synchronously, used in testing mode
        /// </summary>
        public void DoWork()
        {
            if (this.WorkerStartedEvent != null)
            {
                this.WorkerStartedEvent(this._JobTicketID, this, this.GetType(), this._Instruction);
            }
            this._WorkerThread = Thread.CurrentThread;
            this.ScanTableLayouts();
        }


        /// <summary>
        /// this is called by job dispatcher
        /// </summary>
        public void DoWorkAsync()
        {
            if (this.WorkerStartedEvent != null)
            {
                this.WorkerStartedEvent(this._JobTicketID, this, this.GetType(), this._Instruction);
            }
            this._WorkerThread = new Thread(new ThreadStart(this.ScanTableLayouts));
            this._WorkerThread.Start();
        }

        /// <summary>
        /// this is called by job monitor/manager
        /// </summary>
        public void CancelWork()
        {
            if (this._WorkerThread != null && this._WorkerThread.IsAlive)
            {
                this._WorkerThread.Abort();
                if (this.WorkerCancelledEvent != null)
                {
                    this.WorkerCancelledEvent(this._JobTicketID, this.GetType(), this._Instruction);
                }
            }
        }

        public event WorkerStarted WorkerStartedEvent;
        public event WorkerFinished WorkerFinishedEvent;
        public event WorkerHasError WorkerErredEvent;
        public event WorkerCancelled WorkerCancelledEvent;

        public event WorkerStatusChanged WorkerStatusChangedEvent;

        #endregion

        #region work
        private void ScanTableLayouts()
        {
            _SyncObj=new object();
            this._ManualResets= new ManualResetEvent[this._Instruction.ThreadPoolSize];
            this._CurrentWorkloads=new int[this._Instruction.ThreadPoolSize];

            try
            {
                string appFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
                appFolderPath = Path.Combine(appFolderPath, "Workflow data");
                if (!Directory.Exists(appFolderPath))
                {
                    Directory.CreateDirectory(appFolderPath);
                }
                // init 
                if(File.Exists(this._Instruction.TableLayoutOutputFilePath))
                {
                    File.Delete(this._Instruction.TableLayoutOutputFilePath);
                }
                this._OutputWriter=new XmlTextWriter(this._Instruction.TableLayoutOutputFilePath, Encoding.UTF8);
                this._OutputWriter.WriteStartElement("Files");
                if (string.IsNullOrEmpty(this._Instruction.LogFilePath))
                {
                    this._Instruction.LogFilePath = Path.Combine(appFolderPath, "TableLayoutScann.log");
                }
                if (File.Exists(this._Instruction.LogFilePath))
                {
                    File.Delete(this._Instruction.LogFilePath);
                }
                this._LogWriter = new StreamWriter(this._Instruction.LogFilePath);
                if (string.IsNullOrEmpty(this._Instruction.ErrFilePath))
                {
                    this._Instruction.ErrFilePath = Path.Combine(appFolderPath, "TableLayoutScann.err");
                }
                if (File.Exists(this._Instruction.ErrFilePath))
                {
                    File.Delete(this._Instruction.ErrFilePath);
                }
                this._ErrorWriter = new StreamWriter(this._Instruction.ErrFilePath);
                int filesProcessed = 0;
                int filesWithData = 0;

                // col detect settings
                this._ColumnDetectSettings=new Dictionary<int, TableColumnDetectorInstruction>();
                foreach(int applyOrder in this._Instruction.ColumnHeaderOptions.Keys)
                {
                    TableColumnDetectorInstruction colSetting = new TableColumnDetectorInstruction();
                    colSetting.AdjustColumnHeaderWidthFromTableRowCount = 5;
                    colSetting.ColumnHeadersInFile = this._Instruction.ColumnHeaderOptions[applyOrder];
                    colSetting.ColumnHeaderSlidingWindowStep = 5;
                    colSetting.ColumnHeaderSlidingWindwSize = 15;
                    colSetting.ColumnLayoutDetectionType = ColumnBorderDetectorType.SlidingWindow;
                    colSetting.MatchScoreThreshold = 0.7;
                    this._ColumnDetectSettings.Add(applyOrder,colSetting);
                }

                // file list
                this._DocIDFilePathMappings = this.ReadFileList(this._Instruction.FileListInputXmlFilePath);

                // pages containing tables
                this._PagesContainingTable = this.ReadTableDetectorResults(this._Instruction.TableScanResultFile);

                // browse and loop
                if (this._DocIDFilePathMappings != null && this._DocIDFilePathMappings.Count > 0)
                {
                    int[] allDocIDs=new int[this._DocIDFilePathMappings.Count];
                    this._DocIDFilePathMappings.Keys.CopyTo(allDocIDs, 0);
                    int i = 0;
                    while (i < allDocIDs.Length)
                    {
                        List<int> docIDs = new List<int>();
                        for (int workerID = 0; workerID < this._Instruction.ThreadPoolSize; workerID++)
                        {
                            if (i < allDocIDs.Length)
                            {
                                docIDs.Add(allDocIDs[i]);
                                i++;
                            }
                            else
                                break;
                        }
                        this._CurrentWorkloads = docIDs.ToArray();
                        this._ManualResets = new ManualResetEvent[docIDs.Count];
                        for (int k = 0; k < docIDs.Count; k++)
                        {
                            this._ManualResets[k] = new ManualResetEvent(false);
                            ThreadPool.QueueUserWorkItem(new WaitCallback(ScanTableLayoutFromFile), k);
                        }
                        WaitHandle.WaitAll(this._ManualResets);

                        int fileScanned = i;
                        if (this.WorkerStatusChangedEvent != null && fileScanned % 10 == 0)
                        {
                            int percent = fileScanned * 100 / allDocIDs.Length;
                            string filePath = this._DocIDFilePathMappings[allDocIDs[i - 1]];
                            string msg = string.Format("Scanned {0} of {1}: {2}...", fileScanned, allDocIDs.Length,
                                                       filePath);
                            this.WorkerStatusChangedEvent(this._JobTicketID, this.GetType(), this._Instruction, this.WorkflowName, this.ComponentName, msg,
                                                          percent);
                        }
                    }
                }
                this._OutputWriter.WriteEndElement();

                this._Output =
                    new TableLayoutDetectorOutput(
                        this._Instruction.OwnerComponentID, this._JobTicketID,
                        this._Instruction.TableLayoutOutputFilePath,
                        filesProcessed, filesWithData);

                if (this.WorkerFinishedEvent != null)
                {
                    this.WorkerFinishedEvent(this._JobTicketID, this.GetType(), this._Instruction, this._Output);
                }
            }
            catch (Exception ex)
            {
                if (this.WorkerErredEvent != null)
                {
                    this.WorkerErredEvent(this._JobTicketID, this.GetType(), this._Instruction, ex.ToString(),true);
                }
            }
            finally
            {
                this._LogWriter.Flush();
                this._LogWriter.Close();
                this._ErrorWriter.Flush();
                this._ErrorWriter.Close();

                this._OutputWriter.Flush();
                this._OutputWriter.Close();
            }
        }

        /// <summary>
        /// doc id -> scanresult
        /// </summary>
        /// <param name="tableScanOutputFilePath"></param>
        /// <returns></returns>
        private Dictionary<int, PdfScanOutput.PdfScanResult> ReadTableDetectorResults(string tableScanOutputFilePath)
        {
            Dictionary<int, PdfScanOutput.PdfScanResult> tablePages = new Dictionary<int, PdfScanOutput.PdfScanResult>();
            XmlDocument xDoc = new XmlDocument();
            xDoc.Load(tableScanOutputFilePath);
            XmlNodeList fileNodes = xDoc.SelectNodes("//Files/File");
            if (fileNodes != null && fileNodes.Count > 0)
            {
                foreach (XmlNode fileNode in fileNodes)
                {
                    int docID = int.Parse(fileNode.Attributes.GetNamedItem("DocID").Value);
                    PdfScanOutput.PdfScanResult scanResult = new PdfScanOutput.PdfScanResult();
                    scanResult.IsElectronic = bool.Parse(fileNode.Attributes.GetNamedItem("IsElectronic").Value);
                    scanResult.IsScanned = bool.Parse(fileNode.Attributes.GetNamedItem("IsScanned").Value);
                    scanResult.PageNumbers = IntUtil.ToArray(fileNode.Attributes.GetNamedItem("Pages").Value);
                    tablePages.Add(docID, scanResult);
                }
            }
            return tablePages;
        }

        private void ScanTableLayoutFromFile(object pObject)
        {
            int idx = (int) pObject;
            int docID = this._CurrentWorkloads[idx];
            string filePath = this._DocIDFilePathMappings[docID];
            string fileName = Path.GetFileName(filePath);
            try
            {
                if(this._PagesContainingTable.ContainsKey(docID))
                {
                    PdfScanOutput.PdfScanResult scanResult = this._PagesContainingTable[docID];
                    if (scanResult.PageNumbers != null && scanResult.PageNumbers.Length > 0)
                    {
                        Dictionary<int,Dictionary<string,RectangleF>> colHeaderLayoutByPages=new Dictionary<int, Dictionary<string, RectangleF>>();
                        foreach (int pageNum in scanResult.PageNumbers)
                        {
                            foreach(int applyOrder in this._Instruction.ColumnHeaderOptions.Keys)
                            {
                                TableColumnDetectorInstruction colDetectSetting = this._ColumnDetectSettings[applyOrder];
                                Dictionary<string, RectangleF> colHeaderLayout=new Dictionary<string, RectangleF>();
                                try
                                {
                                    colHeaderLayout =
                                        TableColumnDetector.DetectTableColumnLayouts(
                                            filePath, pageNum, colDetectSetting);
                                }
                                catch (Exception)
                                {
                                    colHeaderLayout=new Dictionary<string, RectangleF>();
                                }
                                if(colHeaderLayout !=null && colHeaderLayout.Count>0)
                                {
                                    colHeaderLayoutByPages.Add(pageNum,colHeaderLayout);
                                    break;
                                }
                            }
                        }
                        if(colHeaderLayoutByPages ==null || colHeaderLayoutByPages.Count==0)
                        {
                            lock(this._SyncObj)
                            {
                                this._ErrorWriter.WriteLine(
                                    string.Format("{0}: Unable to detect table layout in file {1}",
                                                  DateTime.Now.ToString(), fileName));
                                this._ErrorWriter.Flush();
                            }
                        }
                        else
                        {
                            lock(this._SyncObj)
                            {
                                this.WriteOutput(this._OutputWriter, docID, colHeaderLayoutByPages);
                            }
                        }
                    }
                }
            }
            catch (Exception ex1)
            {
                lock (this._SyncObj)
                {
                    this._ErrorWriter.WriteLine(
                        string.Format("{0}: {1} - error: {2}",
                                      DateTime.Now.ToString(), fileName, ex1.ToString()));
                    this._ErrorWriter.Flush();
                }
            }
            finally
            {
                this._ManualResets[idx].Set();
            }
        }

        private void WriteOutput(XmlTextWriter outputWriter, int docID, Dictionary<int, Dictionary<string, RectangleF>> colHeaderLayoutsByPages)
        {
            outputWriter.WriteStartElement("File");
            outputWriter.WriteAttributeString("DocID",docID.ToString());
            if(colHeaderLayoutsByPages !=null && colHeaderLayoutsByPages.Count>0)
            {
                foreach(int pageNum in colHeaderLayoutsByPages.Keys)
                {
                    outputWriter.WriteStartElement("Page");
                    outputWriter.WriteAttributeString("PageNumber", pageNum.ToString());
                    Dictionary<string, RectangleF> colHeaderLayout = colHeaderLayoutsByPages[pageNum];
                    foreach(string colHeader in colHeaderLayout.Keys)
                    {
                        outputWriter.WriteStartElement("ColumnHeader");
                        outputWriter.WriteAttributeString("Name",colHeader);
                        RectangleF rect = colHeaderLayout[colHeader];
                        outputWriter.WriteAttributeString("X", rect.X.ToString());
                        outputWriter.WriteAttributeString("Y", rect.Y.ToString());
                        outputWriter.WriteAttributeString("W", rect.Width.ToString());
                        outputWriter.WriteAttributeString("H", rect.Height.ToString());
                        outputWriter.WriteEndElement();
                    }
                    outputWriter.WriteEndElement();
                }
            }
            
            outputWriter.WriteEndElement();
        }

        private Dictionary<int, string> ReadFileList(string fileListXmlFilePath)
        {
            Dictionary<int, string> docIDFilePathMappings = new Dictionary<int, string>();
            XmlDocument xDoc = new XmlDocument();
            xDoc.Load(fileListXmlFilePath);
            XmlNodeList fileNodes = xDoc.SelectNodes("//Files/File");
            if (fileNodes != null && fileNodes.Count > 0)
            {
                foreach (XmlNode fileNode in fileNodes)
                {
                    int docID = int.Parse(fileNode.Attributes.GetNamedItem("DocID").Value);
                    string filePath = fileNode.Attributes.GetNamedItem("FilePath").Value;
                    docIDFilePathMappings.Add(docID, filePath);
                }
            }
            return docIDFilePathMappings;
        }
        #endregion
    }
}
